package jers.util.file;

import static java.net.HttpURLConnection.HTTP_FORBIDDEN;
import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.HttpURLConnection;
import java.net.Socket;
import java.net.SocketOptions;
import java.net.URLDecoder;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import jers.JERS;
import jers.util.Utility;


/**
 * Classe responsavel por manipular as requisicoes no servidor HTTP.
 * 
 * @see {@link SimpleHttpFileServer}
 * 
 * @author 
 * 		<a href="mailto:felipe.zappala@gmail.com">
 * 			Luis Felipe Zappala
 * 		</a> [ <a href="http://lncc.br/ist">ISTCC-P</a> / 2009 ]
 */
class SimpleHttpRequestHandle implements Runnable {

	/** O nome/versoa do servidor HTTP. */
	private static final String SERVER_VERSION;
	static {
		SERVER_VERSION = "JERS-HttpFileServer/1.0.0";
	}
	
	/** Os tipos de conteudos conhecidos. */
	private static final Map<String, String> MIME_TYPES;
    static {
    	MIME_TYPES = new HashMap<String, String>();
    	MIME_TYPES.put("DEFAULT", "application/octet-stream");
    	
        String image = "image/";
        MIME_TYPES.put(".gif",  image + "gif");
        MIME_TYPES.put(".jpg",  image + "jpeg");
        MIME_TYPES.put(".jpeg", image + "jpeg");
        MIME_TYPES.put(".png",  image + "png");
        
        String text = "text/";
        MIME_TYPES.put(".html", text + "html");
        MIME_TYPES.put(".htm",  text + "html");
        MIME_TYPES.put(".txt",  text + "plain");
        
        // Files transporteds
        MIME_TYPES.put(".class", "application/octet-stream");
        MIME_TYPES.put(".jar", "application/java-archive");
        MIME_TYPES.put(".java", text + "plain");
        MIME_TYPES.put(".policy", text + "plain");
        MIME_TYPES.put(".properties", text + "plain");
    }
	
    /** Os diretorios  do conteudo do servidor */
	private Set<File> directories;
	
	/** Socket para conexao com o cliente. */
	private Socket clientSocket;
    
	/**
	 * Construtor padrao.
	 *  
	 * @param 
	 * 		rootDirectory - O diretorio raiz do conteudo do servidor.
	 * @param 
	 * 		clientSocket - Conexao {@link Socket} com o cliente.
	 */
	public SimpleHttpRequestHandle(Set<File> directories, Socket clientSocket) {
		this.directories = directories;
		this.clientSocket = clientSocket;
	}
	
	/**
	 * Manipula a requisicao e desolve um resposta.
	 */
	public void run() {
		try {
			this.clientSocket.setSoTimeout(SocketOptions.SO_TIMEOUT * 10);
			
			BufferedReader in = new BufferedReader(
					new InputStreamReader(clientSocket.getInputStream()));
			BufferedWriter out = new BufferedWriter(
					new OutputStreamWriter(clientSocket.getOutputStream()));

            String request = in.readLine();
            
            JERS.log.info("[SimpleHttpRequestHandle] Request [", request, "]");
            
            if (isInvalidRequest(request)) {
            	sendError(out, HTTP_INTERNAL_ERROR, "Invalid Method.");
            	close(in, out);
            	return;
            }
            
            String path = request.substring(4, request.length() - 9);
            File file = new File(URLDecoder.decode(path.trim(), "UTF-8"));
            
            JERS.log.info("[SimpleHttpRequestHandle] Required file [",
            		file.toString(), "]");
            
//			if (file.isDirectory()) {
//			    File indexFile = new File(file, "index.html");
//			    if (indexFile.exists() && !indexFile.isDirectory()) {
//			        file = indexFile;
//			    }
//			}
            
            if (isDenied(file)) {
                sendError(out, HTTP_FORBIDDEN, "Permission Denied.");
                close(in, out);
                return;
            }
            
            file = getFullPathFile(file);
            
//            if (!file.exists()) {
//                sendError(out, HTTP_NOT_FOUND, "File Not Found.");
//                close(in, out);
//                return;
//            }
            
//			if (file.isDirectory()) {
//				sendDirectory(out, file);
//				close(in, out);
//				return;
//			}
            if (file.isDirectory()) {
            	sendError(out, HTTP_FORBIDDEN, "Permission Denied.");
				close(in, out);
				return;
			}
            
            //
            // Faz a tranferencia do arquivo
            //
            BufferedInputStream inBin = new BufferedInputStream(
            		new FileInputStream(file));
            BufferedOutputStream outBin = new BufferedOutputStream(
            		clientSocket.getOutputStream());
            
            String contentType = MIME_TYPES.get(getFileExtension(file));
            if (contentType == null) {
                contentType = MIME_TYPES.get("DEFAULT");
            }
            
            setHttpHeader(out, HttpURLConnection.HTTP_OK, contentType, 
            		file.length(), file.lastModified());
            
            byte[] buffer = new byte[4096];
            int bytesRead;
            while ((bytesRead = inBin.read(buffer)) != -1) {
            	outBin.write(buffer, 0, bytesRead);
            }
            outBin.flush();
            close(inBin, outBin);
            close(in, null);
            
		} catch (Exception e) {
			JERS.log.error(e, "[SimpleHttpRequestHandle] Execution error.");
		}
	}
	
	private File getFullPathFile(File file) {
		File fullFile;
		for (File dir : directories) {
			fullFile = new File(dir.getPath(), file.getPath());
			if (fullFile.exists()) {
				return fullFile;
			}
		}
		return null;
	}

	/**
	 * Fecha um stream texto de entrada e um de saida
	 * 
	 * @param 
	 * 		in - O {@link Reader} de entrada.
	 * @param 
	 * 		out - O {@link Writer} de saida.
	 * @throws 
	 * 		IOException - Caso ocorra algum erro ao fechar
	 */
	private void close(Reader in, Writer out) throws IOException {
		if (out != null) {
			out.flush();
			out.close();
		}
		if (in != null) {
			in.close();
		}
	}

	/**
	 * Fecha um stream binario de entrada e um de saida
	 * 
	 * @param 
	 * 		in - O {@link InputStream} de entrada.
	 * @param 
	 * 		out - O {@link OutputStream} de saida.
	 * @throws 
	 * 		IOException - Caso ocorra algum erro ao fechar
	 */
	private void close(InputStream in, OutputStream out) throws IOException {
		if (out != null) {
			out.flush();
			out.close();
		}
		if (in != null) {
			in.close();
		}
	}

	/**
	 * Verifica se a requisicao e invalida. 
	 * Esta classe so trata requisicoes to tipo HTTP/GET
	 * 
	 * @param 
	 * 		req - A String de requisicao enviada.
	 * @return
	 * 		Verdadeiro quando a requisicao for invalida.
	 */
	private boolean isInvalidRequest(String req) {
		return req == null 
				|| !(req.startsWith("GET ") || req.startsWith("HEAD ")) 
				|| !(req.endsWith(" HTTP/1.0") || req.endsWith("HTTP/1.1"));
	}


	/**
	 * Envia uma pagina com o conteudo do diretorio selecionado.
	 * 
	 * @param 
	 * 		out - O {@link OutputStream} para a resposta. 
	 * @param 
	 * 		file - O diretorio {@link File} a ser exibido.
	 * @throws 
	 * 		IOException Caso ocorra algum erro de leitura ou escrita.
	 */
	@SuppressWarnings("unused")
	private void sendDirectory(BufferedWriter out, File file) 
			throws IOException {
		String path = file.getPath();
		if (!path.endsWith("/")) {
            path = Utility.concat(path, "/");
        }
		
        File[] files = file.listFiles();
        setHttpHeader(out, HttpURLConnection.HTTP_OK, "text/html", 
        		-1, System.currentTimeMillis());
        
        out.write(Utility.concat("<html><head><title>Index of ", path, 
        		"</title></head><body><h3>Index of ", path, "</h3><p>\n"));
        
        File f;
        for (int i = 0; i < files.length; i++) {
            f = files[i];
            String description = "";
            if (f.isDirectory()) {
                description = "&lt;DIR&gt;";
            }
            out.write(Utility.concat("<a href=\"", path, f.getName(), 
            		"\">", f.getName(), "</a> ", description, "<br>\n"));
        }
        out.write(Utility
        		.concat("</p><hr><p>", SERVER_VERSION, "</p></body><html>"));
	}

	/**
	 * Verifica se o acesso ao arquivo nao e permitido.
	 * 
	 * @param 
	 * 		file - O arquivo a ser verificado
	 * @return
	 * 		Verdadeiro quando o acesso nao for permitido.
	 */
	private boolean isDenied(File file) {
		return getFullPathFile(file) == null;
	}

	/**
	 * Configura o cabecalho do HTTP na resposta.
	 * 
	 * @param 
	 * 		out - O {@link OutputStream} para a resposta.
	 * @param 
	 * 		code - O codigo do HTTP
	 * @param 
	 * 		contentType - O tipo de conteudo.  
	 * @param 
	 * 		contentLength - O comprimento do conteudo.
	 * @param 
	 * 		lastModified - O tempo da ultima modificacao. 
	 * @throws 
	 * 		IOException caso ocorra algum erro
	 */
	private static void setHttpHeader(BufferedWriter out, int code, 
			String contentType, long contentLength, long lastModified) 
			throws IOException {
		StringBuilder sb = new StringBuilder();
		String END = "\r\n";
		Date date = new Date();
		
		sb.append("HTTP/1.0 ").append(code).append(" OK").append(END);
		sb.append("Date: ").append(date.toString()).append(END);
		sb.append("Server: ").append(SERVER_VERSION).append(END);
		sb.append("Content-Type: ").append(contentType).append(END);
		sb.append("Expires: Thu, 01 Dec 1994 16:00:00 GMT").append(END);
		if (contentLength != -1) {
			sb.append("Content-Length: ").append(contentLength).append(END);
		}
		date = new Date(lastModified);
		sb.append("Last-modified: ").append(date.toString()).append(END);
		sb.append(END);
		
		out.write(sb.toString());
    }
	
	/**
	 * Envia uma resposta de erro.
	 * 
	 * @param 
	 * 		out - O {@link OutputStream} para a resposta.
	 * @param 
	 * 		code - O codigo de erro do HTTP.
	 * @param 
	 * 		message - A mesagem de erro.
	 * @throws 
	 * 		IOException caso ocorra algum erro.
	 */
	private static void sendError(BufferedWriter out, int code, String message) 
			throws IOException {
		JERS.log.info("[SimpleHttpRequestHandle] sendError [", message, "]");
		String msg = Utility.concat(message, "<hr />", SERVER_VERSION);
		
		long time = System.currentTimeMillis();
        setHttpHeader(out, code, "text/html", msg.length(), time);
        out.write(msg);
    }

	/**
	 * Obtem a extensao do arquivo.
	 * 
	 * @param 
	 * 		file - O arquivo a se obter a extensao.
	 * @return 
	 * 		A {@link String} da extensao do arquivo com ponto (Ex.: .html)
	 */
	private static String getFileExtension(File file) {
        String extension = "";
        String filename = file.getName();
        int dotPos = filename.lastIndexOf(".");
        if (dotPos >= 0) {
            extension = filename.substring(dotPos);
        }
        return extension.toLowerCase();
    }
	
}
